package display;

import java.util.*;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.IndexColorModel;
import java.awt.image.MemoryImageSource;
import java.io.File;
import java.io.FileOutputStream;

import javax.swing.JPanel;

import net.sf.epsgraphics.EpsGraphics;

import algorithm.Algorithm;

/**
 * @author gabriella
 *
 */
public class CubeDisplay extends JPanel implements Display, MouseListener,
		MouseMotionListener, Runnable {

	JPanel parent = null;
	Algorithm rule;

	Thread thCycle;
	int w, h, w2, h2, mx0, my0, delay = 1000, it, gens, 
			Rred = 6, Rgr = 6, n = 11, nvert, zs[];
	float ax[], ay[], az[];
	double fiX = .3, fiY = .3, dfi, scaleX, scaleZ, Ro;
	Image buffImage, balls[], pBalls[];
	Graphics buffGraphics;
	private final static int bgGrey = 192;
	private final static int nBalls = 16;
	byte L[]; 
	boolean bCycle = false, bSet = false;
	
	private List<DisplayListener> listeners = new ArrayList<DisplayListener>();
	/**
	 * 
	 * @param meret,size of the cube 
	 * @param rule  
	 * @param delay, delay between itarations
	 * @param generations
	 * @param bshow
	 * @param parent
	 */
	public CubeDisplay(int meret, Algorithm rule, int delay, int generations, boolean bshow, JPanel parent) {
		this.parent = parent;
		n = meret;
		this.delay = delay;
		gens = generations;
		bSet = bshow;
		this.rule = rule;
		it = 0; // number of iterations performed
		setBackground(Color.WHITE);
		setVisible(true);
	}
	
	public void setupSize() {
		w = parent.getSize().width;
		h = parent.getSize().height;
		setSize(w, h);
		dfi = 6.28 / w;
		w2 = w / 2;
		h2 = h / 2;
		buffImage = createImage(w, h);
		buffGraphics = buffImage.getGraphics();
		setBackground(Color.white);
		Setup(Rgr, 0, 0, 255);
		pBalls = balls;
		Setup(Rred, 255, 0, 0);
		scaleX = w * .6 / n;
		scaleZ = nBalls / (1.75 * n);
		Ro = ((double) n / 2 - .5);
		rotate();
	}
	/**
	 * initialize the cube (calls rule.init)
	 */
	public void init() {
		
		nvert = n * n * n;
		
		zs = new int[nvert];
		ax = new float[nvert];
		ay = new float[nvert];
		az = new float[nvert];
		for (int i = 0; i < nvert; i++)
			zs[i] = i;
		
		
		L = rule.init();

		it = 0;
		
		setupSize();

		addMouseListener(this);
		addMouseMotionListener(this);
		
		for (DisplayListener hl : listeners) {
            hl.setIterationCount(it);
            hl.setPopulationCount(rule.getC(), rule.getD());
		}
		
		repaint();
	}

	public void destroy() {
		removeMouseListener(this);
		removeMouseMotionListener(this);
	}

	@Override
	public void run() {
		while (it < gens) {
			if (Thread.currentThread() != thCycle)
				return;
			try {
				Thread.sleep(delay);
			} catch (InterruptedException e) {
			}
			cycle();
			repaint();
		}
		stopSimulation();
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		int x1 = e.getX();
		int y1 = e.getY();
		fiY += dfi * (x1 - mx0);
		mx0 = x1;
		fiX += dfi * (y1 - my0);
		my0 = y1;
		rotate();
		repaint();
		e.consume();
	}

	@Override
	public void mouseMoved(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseClicked(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseReleased(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	public void paint(Graphics g) {
		//buffGraphics.clearRect(0, 0, w, h);
		buffGraphics.setColor(Color.WHITE);
		buffGraphics.fillRect(0, 0, w, h);
		buffGraphics.setColor(Color.BLACK);
		int xo = w2 - Rred, yo = h2 - Rred, xp = w2 - Rgr, yp = h2 - Rgr;
		for (int i = nvert - 1; i >= 0; i--) {
			int j = zs[i];
			if (L[j] == 0) {
				if (bSet)
					buffGraphics.drawImage(pBalls[(int) az[j]], xp
							+ (int) ax[j], yp + (int) ay[j], this);
				else {
					int xi = w2 + (int) ax[j], yi = h2 + (int) ay[j];
					buffGraphics.drawLine(xi, yi, xi, yi);
				}
			} else {
				buffGraphics.drawImage(balls[(int) az[j]], xo + (int) ax[j], yo
						+ (int) ay[j], this);
			}
		}
		g.drawImage(buffImage, 0, 0, this);
	}

	public void update(Graphics g) {
		paint(g);
	}

	void cycle() {
		it++;
		rule.cycle(L);
		repaint();
		for (DisplayListener hl : listeners) {
            hl.setIterationCount(it);
            hl.setPopulationCount(rule.getC(), rule.getD());
		}
	}
	/**
	 * rotate the cube
	 */
	private void rotate() {
		int p = 0;
		double cx = Math.cos(-fiX), cy = Math.cos(-fiY), sx = Math.sin(-fiX), sy = Math
				.sin(-fiY), dix = scaleX * cy, diy = -scaleX * sx * sy, diz = -scaleZ
				* cx * sy, djy = scaleX * cx, djz = -scaleZ * sx, dkx = scaleX
				* sy, dky = scaleX * sx * cy, dkz = scaleZ * cx * cy;
		double ix = -Ro * (dix + dkx), iy = -Ro * (diy + djy + dky), iz = -Ro
				* (diz + djz + dkz) + nBalls / 2;
		for (int i = 0; i < n; i++) {
			double jy = 0, jz = 0;
			for (int j = 0; j < n; j++) {
				double kx = 0, ky = 0, kz = 0;
				for (int k = 0; k < n; k++) {
					ax[p] = (float) (ix + kx);
					ay[p] = (float) (iy + jy + ky);
					az[p++] = (float) (iz + jz + kz);
					kx += dkx;
					ky += dky;
					kz += dkz;
				}
				jy += djy;
				jz += djz;
			}
			ix += dix;
			iy += diy;
			iz += diz;
		}
		for (int i = nvert - 1; --i >= 0;) {
			boolean flipped = false;
			for (int j = 0; j <= i; j++) {
				int a = zs[j], b = zs[j + 1];
				if (az[a] > az[b]) {
					zs[j + 1] = a;
					zs[j] = b;
					flipped = true;
				}
			}
			if (!flipped)
				break;
		}
	}

	private int blend(int fg, int bg, float fgfactor) {
		return (int) (bg + (fg - bg) * fgfactor);
	}

	private void Setup(int R, int Rl, int Gl, int Bl) {
		int R2 = 2 * R, hx = R / 4, hy = hx;
		byte[] data = new byte[R2 * R2];
		int mr = 0;
		for (int Y = R2; --Y >= 0;) {
			int x0 = (int) (Math.sqrt(R * R - (Y - R) * (Y - R)) + 0.5);
			int p = Y * R2 + R - x0;
			for (int X = -x0; X < x0; X++) {
				int x = X + hx;
				int y = Y - R + hy;
				int r = (int) (Math.sqrt(x * x + y * y) + 0.5);
				if (r > mr)
					mr = r;
				data[p++] = r <= 0 ? 1 : (byte) r;
			}
		}
		balls = new Image[nBalls];
		byte red[] = new byte[256];
		red[0] = (byte) bgGrey;
		byte green[] = new byte[256];
		green[0] = (byte) bgGrey;
		byte blue[] = new byte[256];
		blue[0] = (byte) bgGrey;
		for (int r = 0; r < nBalls; r++) {
			float b = (float) (r + 1) / nBalls;
			for (int i = mr; i >= 1; --i) {
				float d = (float) i / mr;
				red[i] = (byte) blend(blend(Rl, 255, d), bgGrey, b);
				green[i] = (byte) blend(blend(Gl, 255, d), bgGrey, b);
				blue[i] = (byte) blend(blend(Bl, 255, d), bgGrey, b);
			}
			IndexColorModel model = new IndexColorModel(8, mr + 1, red, green,
					blue, 0);
			balls[nBalls - r - 1] = createImage(new MemoryImageSource(R2, R2,
					model, data, 0, R2));
		}
	}

	@Override
	public void startSimulation() {
		thCycle = new Thread(this);
		thCycle.start();
	}

	@Override
	public void stopSimulation() {
		for (DisplayListener hl : listeners)
            hl.simulationStopped();
		thCycle = null;
	}

	@Override
	public boolean isReady() {
		// TODO Auto-generated method stub
		return it >= gens;
	}
	
	public void addListener(DisplayListener toAdd) {
        listeners.add(toAdd);
    }

	@Override
	public void abort() {
		it = gens + 1;
		stopSimulation();
	}
	/**
	 * @see display.Display#saveScreenShot(File f)
	 */
	@Override
	public boolean saveScreenShot(File f) {

		boolean success = true;

		FileOutputStream outputStream = null;
		EpsGraphics eps = null;
		try {
			outputStream = new FileOutputStream(f);
			Dimension d = getSize();
			eps = new EpsGraphics("Diagram", outputStream, 0, 0, d.width,
					d.height, net.sf.epsgraphics.ColorMode.COLOR_RGB);
			paint(eps);
		} catch (Exception ex) {
			ex.printStackTrace();
			success = false;
		} finally {
			try {
				eps.flush();
				eps.close();
			} catch (Exception ex) {
				ex.printStackTrace();
				success = false;
			}

		}

		return success;

	}

}
